﻿using System;

using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Drawing;
using Silvermoon.Drawing;
using Silvermoon.OpenGL.Native;
using Format = Silvermoon.OpenGL.Native.GlesPixelFormat;
using ValueType = Silvermoon.OpenGL.Native.glValueType;
using System.Drawing.Imaging;
using System.Diagnostics;
using System.Threading;
using System.Reflection;

namespace Silvermoon.OpenGL
{
    public class Texture : IDisposable
    {
        const int one = 65536;
        #region statics

        public static void Cleanup()
        {

        }

        #endregion
        #region Fields

        private static int maxTextureSize;
        private int[] textCoords = new int[] { one, 0, one, -one, 0, 0, 0, -one };


        private static IImagingFactory imageFactory;

        #endregion
        #region ctor

        public Texture()
            : base()
        {
            //Textures.Add(this);
            this.Name = gl.GenTexture();
            if (this.Name == 0) throw new OpenGlException("glGenTexture.");
        }

        public virtual void Dispose()
        {
            GC.SuppressFinalize(this);
            if (Name != 0)
            {
                gl.DeleteTexture(Name);
            }
            Name = 0;
        }

        ~Texture()
        {
            Dispose();
        }


        #endregion
        #region Properties

        /// <summary>
        /// When set to false, each color in the texture is multiplied with the color value of a shape with the formula value=(textureValue*colorValue)/255,
        /// if set to true, each color in the texture is considered relative from color (128,128,128). which means that a color in the texture
        /// is calculated as value = (textureValue-128)*colorValue/128.
        /// </summary>
        public bool IsSigned { get; set; }

        /// <summary>
        /// Gets the maximum size for textures. This is usually a limit of 1024 pixel, but depends on the hardware.
        /// </summary>
        public static int MaxTextureSize
        {
            get
            {
                if (maxTextureSize == 0)
                {
                    maxTextureSize = gl.GetIntegerValue(ParameterName.MaxTextureSize);
                }
                return maxTextureSize;
            }
        }

        public uint Name { get; protected set; }

        /// <summary>
        /// Gets the size that is used to store the image. this is a size that is always 2^n.
        /// </summary>
        public Size StorageSize { get; private set; }

        /// <summary>
        /// Gets the original size of the image.
        /// </summary>
        public Size Size { get; private set; }

        /// <summary>
        /// Gets the current storage format;
        /// </summary>
        public Format Format { get; private set; }

        /// <summary>
        /// Gets the current value type.
        /// </summary>
        public ValueType ValueType { get; private set; }

        public int Width { get { return Size.Width; } }
        public int Height { get { return Size.Height; } }

        protected static IImagingFactory ImageFactory
        {
            get
            {
                if (imageFactory == null)
                {
                    imageFactory = (IImagingFactory)Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("327ABDA8-072B-11D3-9D7B-0000F81EF32E")));
                }
                return imageFactory;
            }
        }

        #endregion
        #region private Methods

        public DrawingCanvas CreateCanvas(int width, int height, TextureFormat format)
        {
            return new DrawingCanvas(this, width, height, format, Color.Transparent);
        }


        /// <summary>
        /// Creates a new graphics for drawing. Note that an existing image will be deleted.
        /// </summary>
        /// <param name="width">The width of the new  graphics.</param>
        /// <param name="height">The height of the new graphics.</param>
        /// <param name="format">The pixel format.</param>
        /// <param name="transparentColor">The color that specifies the transparent color.</param>
        /// <returns>A new graphics.</returns>
        public DrawingCanvas CreateCanvas(int width, int height, TextureFormat format, Color transparentColor)
        {
            return new DrawingCanvas(this, width, height, format, transparentColor);
        }


        internal unsafe byte[] GetBytesFromBitmap(Bitmap bitmap, TextureFormat textureFormat, Color transparentColor)
        {
            Size size = new Size(bitmap.Width, bitmap.Height);
            if (size.Width > MaxTextureSize) throw new OverflowException("Width exceeds system limit of " + MaxTextureSize.ToString());
            if (size.Height > MaxTextureSize) throw new OverflowException("Height exceeds system limit of " + MaxTextureSize.ToString());

            PixelFormat pixFormat = TextureUtil.GetPixelFormatFromTextureFormat(textureFormat);
            System.Drawing.Imaging.BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, pixFormat);
            try
            {
                IntPtr scan0;

                if (textureFormat != TextureFormat.HiResAlpha)
                {
                    scan0 = ConvertData(data, transparentColor, Size.Width, Size.Height, textureFormat);

                    glValueType valueType = TextureUtil.GetValueTypeFromTextureFormat(textureFormat);
                    GlesPixelFormat glFormat = TextureUtil.GetGlFormatFromTextureFormat(textureFormat);
                    BitmapImageData imgData = new BitmapImageData(data);
                    ReformatRawData(textureFormat, imgData);
                }
                else scan0 = data.Scan0;

                int n = data.Height * data.Stride;
                if (textureFormat == TextureFormat.HiResAlpha) n = n / 3;

                byte[] result = new byte[n];

                if (textureFormat == TextureFormat.HiResAlpha)
                {
                    byte* cc = (byte*)((int)data.Scan0);
                    for (int i = 0, j = 0; i < result.Length; i++, j += 3)
                    {
                        result[i] = cc[j];
                    }
                }
                else
                {
                    byte* cc = (byte*)((int)data.Scan0);
                    for (int i = 0; i < result.Length; i++)
                    {
                        result[i] = *cc;
                        cc++;
                    }
                }
                return result;
            }
            finally
            {
                bitmap.UnlockBits(data);
            }
        }

        internal void CopyBitmap(Bitmap bitmap, TextureFormat textureFormat, Color transparentColor, Size origSize)
        {
            this.Size = origSize;
            Size size = new Size(bitmap.Width, bitmap.Height);
            if (size.Width > MaxTextureSize) throw new OverflowException("Width exceeds system limit of " + MaxTextureSize.ToString());
            if (size.Height > MaxTextureSize) throw new OverflowException("Height exceeds system limit of " + MaxTextureSize.ToString());

            PixelFormat pixFormat = TextureUtil.GetPixelFormatFromTextureFormat(textureFormat);
            System.Drawing.Imaging.BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, pixFormat);
            try
            {
                IntPtr scan0 = ConvertData(data, transparentColor, origSize.Width, origSize.Height, textureFormat);

                glValueType valueType = TextureUtil.GetValueTypeFromTextureFormat(textureFormat);
                GlesPixelFormat glFormat = TextureUtil.GetGlFormatFromTextureFormat(textureFormat);
                BitmapImageData imgData = new BitmapImageData(data);
                ReformatRawData(textureFormat, imgData);
                LoadPixels(scan0, size, glFormat, valueType);
            }
            finally
            {
                bitmap.UnlockBits(data);
            }
        }

        private IntPtr ConvertData(System.Drawing.Imaging.BitmapData data, Color transparentColor, int width, int height, TextureFormat format)
        {
            switch (format)
            {
                //case TextureFormat.Alpha:
                //    return TextureUtil.GenerateAlphaPlaneFromRgb565(data, width, height);

                case TextureFormat.LuminanceAlpha:
                    return TextureUtil.GenerateAlphaPlaneFromARGB(data, transparentColor, width, height);

                case TextureFormat.RGBA5551:
                    switch (data.PixelFormat)
                    {
                        case PixelFormat.Format32bppRgb: return TextureUtil.ConvertRGBATo5551(data, transparentColor);
                        case PixelFormat.Format16bppRgb555: return TextureUtil.ConvertRGB555toRGBA5551(data, transparentColor);
                        default: throw new NotSupportedException();
                    }


                default:
                    return data.Scan0;
            }
        }



        public static Texture FromStream(Stream stream, TextureFormat textureFormat)
        {
            Texture texture = new Texture();
            texture.LoadFromStream(stream, textureFormat);
            return texture;
        }

        public static Texture FromStream(Stream stream)
        {
            Texture texture = new Texture();
            texture.LoadFromStream(stream, TextureFormat.Automatic);
            return texture;
        }


        /// <summary>
        /// Loads data from a stream.
        /// </summary>
        /// <param name="stream">The stream where to get the image data.</param>
        /// <param name="textureFormat">The format of the texture.</param>
        public void LoadFromStream(Stream stream, TextureFormat textureFormat)
        {
            if (stream == null) throw new ArgumentNullException("stream");
            int bytesLength = (int)stream.Length;
            byte[] bytes = TextureUtil.BytesFromStream(stream);

            IImage image;
            uint hresult = ImageFactory.CreateImageFromBuffer(bytes, (uint)bytesLength, BufferDisposalFlag.None, out image);
            try
            {
                ImageInfo info = GetImageInfo(image);
                this.Size = new Size(info.Width, info.Height);
                Size imageSize = DetermineSize(info);

                if (textureFormat == TextureFormat.Automatic) textureFormat = GetTextureFormat(info);
                RawPixelFormat rawPixelFormat = TextureUtil.GetRawPixelFormatFromTextureFormat(textureFormat, (info.Flags & SinkFlags.HasAlpha) != 0);

                IBitmapImage bitmap;
                ImageFactory.CreateBitmapFromImage(image, (uint)imageSize.Width, (uint)imageSize.Height, rawPixelFormat, InterpolationHint.Default, out bitmap);
                try
                {
                    Size textureSize;
                    bitmap.GetSize(out textureSize);
                    //StorageSize = textureSize;
                    RECT rect = new RECT(0, 0, textureSize.Width, textureSize.Height);
                    GlesPixelFormat glFormat = TextureUtil.GetGlFormatFromTextureFormat(textureFormat);
                    glValueType valueType = TextureUtil.GetValueTypeFromTextureFormat(textureFormat);

                    glImageLockMode lockMode = glImageLockMode.Read;
                    BitmapImageData data = new BitmapImageData();
                    bitmap.LockBits(ref rect, lockMode, rawPixelFormat, data);
                    try
                    {
                        IntPtr scan0 = data.Scan0;

                        ReformatRawData(textureFormat, data);
                        LoadPixels(scan0, textureSize, glFormat, valueType);
                    }
                    finally
                    {
                        bitmap.UnlockBits(data);
                    }
                }
                finally
                {
                    Marshal.FinalReleaseComObject(bitmap);
                }
            }
            finally
            {
                Marshal.FinalReleaseComObject(image);
            }
        }

        private static ImageInfo GetImageInfo(IImage image)
        {
            ImageInfo info = new ImageInfo();
            image.GetImageInfo(info);
            return info;
        }

        private void ReformatRawData(TextureFormat textureFormat, BitmapImageData data)
        {
            RawPixelFormat pixelFormat = data.PixelFormat;
            switch (textureFormat)
            {
                case TextureFormat.Alpha:
                    switch (pixelFormat)
                    {
                        case RawPixelFormat.Format16bppRGB565:
                            TextureUtil.GenerateAlphaPlaneFromRgb565(data, data.Width, data.Height);
                            break;

                        case RawPixelFormat.Format32bppARGB:
                            TextureUtil.GenerateAlphaPlaneFromARGB(data);
                            break;

                        default:
                            throw new NotSupportedException();
                    }
                    break;

                case TextureFormat.RGB24:
                    TextureUtil.SwapRedBluePixels(data, 3);
                    break;

                case TextureFormat.RGBA4444:
                    TextureUtil.SwapRedBluePixels(data, 4);
                    break;

                case TextureFormat.RGBA5551:
                    switch (pixelFormat)
                    {
                        case RawPixelFormat.Format16bppARGB1555: TextureUtil.Convert1555To5551(data); break;
                    }
                    break;

                case TextureFormat.LuminanceAlpha:
                    TextureUtil.ConvertRGBAToLuminanceAlpha(data);
                    break;

                case TextureFormat.Luminance:
                    TextureUtil.ConvertRGBToLuminance(data);
                    break;

                case TextureFormat.HiResAlpha:
                    TextureUtil.ConvertRGBToLuminance(data);
                    //                    TextureUtil.ConvertRGBAToAlpha(data);
                    break;

            }
        }


        private static TextureFormat GetTextureFormat(ImageInfo info)
        {
            if ((info.Flags & SinkFlags.HasAlpha) != 0) return TextureFormat.RGBA5551;
            return TextureFormat.RGB565;
        }



        private static Size DetermineSize(ImageInfo info)
        {
            Size size = new Size(info.Width, info.Height);

            size.Width = TextureUtil.ConvertToValidTextureDimension(size.Width);
            size.Height = TextureUtil.ConvertToValidTextureDimension(size.Height);

            return size;
        }


        private void LoadPixels(IntPtr pixels, Size size, Format glFormat, ValueType glType)
        {
            gl.BindTexture(TextureTarget.Texture2D, Name);
            gl.TexParameterx(TextureTarget.Texture2D, TextureFilter.Minifying, TextureParam.Linear);
            gl.TexParameterx(TextureTarget.Texture2D, TextureFilter.Magnification, TextureParam.Linear);
            gl.TexParameterx(TextureTarget.Texture2D, TextureFilter.WrapS, TextureParam.Repeat);
            gl.TexParameterx(TextureTarget.Texture2D, TextureFilter.WrapT, TextureParam.Repeat);
            gl.GetError();

            //gl.PixelStorei(PixelStoreParam.UnpackAlignment, PixelStoreValue.EvenNumberedAlignment);
            //gl.GetError();

            gl.TexImage2D(TextureTarget.Texture2D, 0, glFormat, size.Width, size.Height, 0, glFormat, glType, pixels);
            gl.CheckError();
            this.StorageSize = size;
            this.Format = glFormat;
            this.ValueType = glType;
        }

        #endregion
        #region Methods

        public void PreLoad()
        {
        }


        /// <summary>
        /// Captures the specified clip from surface.
        /// </summary>
        /// <param name="clip">The clip rectangle to capture into this texture.</param>
        public void Capture(Rectangle clip)
        {
            Capture(clip, TextureUnit.Texture0);
            //this.ImageSize = this.Size = clip.Size;
            SetTextCoords();

        }

        /// <summary>
        /// Captures the specified clip from surface.
        /// </summary>
        /// <param name="clip">The clip rectangle to capture into this texture.</param>
        /// <param name="unit">A valid texture unit to use.</param>
        private void Capture(Rectangle clip, TextureUnit unit)
        {
            gl.ActiveTexture(unit);
            gl.ClientActiveTexture(unit);
            gl.BindTexture(TextureTarget.Texture2D, this.Name);
            Size defSize = new Size(TextureUtil.ConvertToValidTextureDimension(clip.Width), TextureUtil.ConvertToValidTextureDimension(clip.Height));
            short[] buffer = new short[defSize.Width * defSize.Height];
            if (StorageSize != clip.Size)
            {
                LoadPixels(IntPtr.Zero, defSize, GlesPixelFormat.RGB, glValueType.UnsignedShort565);
            }
            uint error2 = gl.GetError();
            if (error2 != 0) throw new OpenGlException("Capture:" + error2.ToString());
            // Note: CopyTexImage2D does not work when the width is larger than the screen width, but CopyTexSubImage2D does.
            //gl.CopyTexImage2D(TargetTexture.Texture2D, 0, GlesPixelFormat.RGB, clip.Left, clip.Top, clip.Width, clip.Height, 0);
            gl.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, clip.Left, clip.Top, clip.Width, clip.Height);
            uint error = gl.GetError();
            if (error != 0) throw new OpenGlException("Capture: " + error.ToString());
            this.Size = clip.Size;
        }

        private void SetTextCoords()
        {
            const int one = 65536;

            int w = StorageSize.Width;
            int h = StorageSize.Height;

            w = w > 0 ? one * Size.Width / w : 0;
            h = h > 0 ? one * (Size.Height - h) / h : 0;

            textCoords[0] = textCoords[2] = w;
            textCoords[1] = textCoords[5] = h;
        }

        public void Render()
        {
            gl.BindTexture(TextureTarget.Texture2D, Name);
            gl.TexCoordPointer(2, glValueType.Fixed, 0, textCoords);
        }

        public static Texture FromResource(string resource, TextureFormat format)
        {
            Stream stream = Assembly.GetCallingAssembly().GetManifestResourceStream(resource);
            if (stream == null) throw new SystemException("Resource " + resource + " not found.");
            Texture image = Texture.FromStream(stream, format);

            return image;
        }

        public static Texture FromResource(string resource, TextureFormat format, bool isSigned)
        {
            Stream stream = Assembly.GetCallingAssembly().GetManifestResourceStream(resource);
            if (stream == null) throw new SystemException("Resource " + resource + " not found.");
            Texture image = Texture.FromStream(stream, format);
            image.IsSigned = isSigned;
            return image;
        }

        internal unsafe void FromByteArray(byte[] array, TextureFormat textureFormat, Size size)
        {
            this.Size = size;
            glValueType valueType = TextureUtil.GetValueTypeFromTextureFormat(textureFormat);
            GlesPixelFormat glFormat = TextureUtil.GetGlFormatFromTextureFormat(textureFormat);

            fixed (byte* b = &array[0])
            {
                IntPtr ptr = new IntPtr(b);
                LoadPixels(ptr, size, glFormat, valueType);
            }
        }

        #endregion

    }
}
